"""
PB-ROE策略
- 综合估值因子
    * 股票池：重资产股票，剔除净利润小于1000万股票。从中证800+中证1000成分股进行
    * 基本面因子：综合EP因子、综合BP因子、盈利超预期因子、股息率因子、研发费用因子、现金净利润占总资产比例
- ROE筛选：剔除小于8%的股票
- 间隔期间动量、波动率因子增强
******** 结论 *******************
    从回测效果来看，组合在银行股上具有超配权重
    分为红利低波策略、成长股策略、中小盘动量策略
"""
import datetime
import numpy as np
import pandas as pd
from utilities.utilities_func import get_price, get_session
from backTestAnaly.bactestEngine import BackTestEngine
from data_resource.data_bases import engine, FactorReturnWgt, FactorVolskew, FactorRVI, FactorPE, FactorPB
from utilities.utilities_statics import factor_standardize
from utilities.utilities_func import set_limit_type, get_next_tradeDate
from backTestAnaly.dataLoader import DataLoader


def stockPool_raw(pool_type='csi800_1000'):
    """获取初始股票池"""
    assert pool_type in ['csi800_1000', ]

    if pool_type == 'csi800_1000':
        _sql = """
        select con_code 
        from quant_research.index_constituent 
        where trade_date=(
            select max(trade_date) from quant_research.index_constituent
        ) and index_code in ('000852.SH', '000906.SH')
        """
        pool = pd.read_sql(_sql, engine)
        return pool['con_code'].tolist()


def get_rebalance(financial_dates: list, n=10):
    """
        再两个财务报告期中间，做因子增强。提高策略适应性
        * 在报告期因子计算完成后，立刻换仓
        financial_dates: 日期列表，"%Y-%m-%d"
    """
    if len(financial_dates) < 4:
        raise ValueError('至少需要2个报告期间隔')
    _start = min(financial_dates)
    _end = max(financial_dates)

    # 获取交易日历
    _sql = f"""
    select cal_date, is_open, pretrade_date
    from quant_research.basic_trading_date
    where cal_date between '{_start}' and '{_end}'
    """
    trading = pd.read_sql(_sql, engine)
    trading.sort_values(by='cal_date', ascending=True, inplace=True)

    _financials1 = financial_dates[:-1]
    _financials2 = financial_dates[1:]

    rebalance_dates = []
    for i, j in zip(_financials1, _financials2):
        _trading = trading[
            (trading['cal_date'] > pd.to_datetime(i).date()) & (trading['cal_date'] < pd.to_datetime(j).date()) & (
                    trading['is_open'] == 1)]  # 获取2个财务报告期之间的交易日
        _trading.reset_index(drop=True, inplace=True)

        if get_next_tradeDate(pd.to_datetime(i).date(), trading, 0):
            # 如果起始财务报告期为交易日，则将次日作为换仓起点
            rebalance_dates.append(_trading.iloc[0, 0])
            _trading1 = _trading.iloc[1:, :]
        else:
            _trading1 = _trading

        nn = n - 1
        while nn < len(_trading1):
            if (nn + 1) % n == 0:
                _date = _trading1.iloc[nn, 0]
                rebalance_dates.append(_date)
            nn += 1

    rr = [pd.to_datetime(d).date() for d in rebalance_dates]
    return sorted(rr)


def get_prev_financialdate(start_year: int, end_year: int, current_year=None) -> dict:
    """
    从开始年份到结束年份，生成数组{后一交易日：报告期}
    当前年份需要手动添加至current_year变量：{'2025-03-31': '2025-04-30'}
    """
    if current_year is None:
        current_year = {}

    _start = f"{start_year}-01-01"
    _end = f"{end_year}-12-31"
    # 获取交易日历
    _sql = f"""
    select cal_date, is_open, pretrade_date
    from quant_research.basic_trading_date
    where cal_date between '{_start}' and '{_end}'
    """
    trading = pd.read_sql(_sql, engine)
    trading.sort_values(by='cal_date', ascending=True, inplace=True)

    _financials = {}
    for i in range(start_year, end_year + 1):
        if i < datetime.date.today().year:
            _financials.update({str(i) + '-03-31': str(i) + '-04-30'})
            _financials.update({str(i) + '-06-30': str(i) + '-08-31'})
            _financials.update({str(i) + '-09-30': str(i) + '-10-31'})
        else:
            continue

    assert isinstance(current_year, dict)
    if len(current_year) > 0:
        _financials.update(current_year)

    _r = {}
    for financial_date in _financials.keys():
        _date = _financials[financial_date]
        if get_next_tradeDate(pd.to_datetime(_date).date(), trading, 0):
            _r.update({_date: financial_date})
        else:
            _date = get_next_tradeDate(pd.to_datetime(_date).date(), trading, 1).strftime('%Y-%m-%d') # 最后财务披露日期非交易日，则返回下一交易日
            _r.update({_date: financial_date})
    return _r


# 策略类
class PeROE(BackTestEngine):
    def __init__(self, price: DataLoader, rebalance_dates: list, financial_date: dict,
                 benchmark_data: pd.DataFrame = None,
                 cash=100000):
        """
            price: 行情价格参数，columns=[trading, code, close, open, volume]
        """
        super().__init__(data=price, cash=cash, benchmark_data=benchmark_data)  # 注入交易日数据
        self.strategy_pool = None  # 每报告期截面股票池
        self.init_pool = stockPool_raw()  # 初始股票池
        self.rebalance_dates = rebalance_dates  # 调仓日期, 通过get_rebalance()函数获取
        self.financial_date = financial_date  # 报告期数组 {key: 交易日, value: 报告期}，若报告期为非交易日，则key为后一交易日
        # 设置因子数据, columns = [code, trading, signal]

        # 组织价值的变化
        self.total_portvalue = {}

    def __next__(self):
        result = super().__next__()  # 获取父类迭代器返回的：下一数据帧索引

        # 处理未完成订单
        if len(self.paramsPipe.stocks_unsettled) > 0:
            _unsettled_stocs = self.paramsPipe.get_stocks_unsettled()
            _unsettled_stocs.sort_values(by='direction', ascending=True, inplace=True)
            _unsettled = []
            for i in range(len(_unsettled_stocs)):
                _code = _unsettled_stocs.iloc[i, 0]
                _size = _unsettled_stocs.iloc[i, 1]
                _price = _unsettled_stocs.iloc[i, 2]
                _direction = _unsettled_stocs.iloc[i, 3]
                _limit_type = set_limit_type(_code)

                # 下单
                if _direction == -1:
                    self.order.sell(_code, result, _limit_type, _price, _size)
                    if self.order.order_status != 1:
                        _unsettled.append((_code, _size, _price, _direction))

                elif _direction == 1:
                    self.order.buy(_code, result, _limit_type, _price, _size)
                    if self.order.order_status != 1:
                        _unsettled.append((_code, _size, _price, _direction))
            # 对未完成订单进行替换
            self.paramsPipe.update_stocks_unsettled(_unsettled)

        """策略主体"""
        if self.today in self.rebalance_dates:
            # 属于调仓日，计算截面股票的动量因子并计算
            """
            计算因子：60日换手率加权累计收益率 factor_returnWgt；20日成交量加权收益率偏度; 
                * 估值综合因子: ep_pivot_250(正向), ep(正向)
            """
            # 清空未完成订单
            self.paramsPipe.clear_stocks_unsettled()

            with get_session(engine) as session:
                _f = session.query(
                    FactorReturnWgt.trading,
                    FactorReturnWgt.code,
                    FactorReturnWgt.signal,
                    FactorVolskew.signal_volReturn
                ).join(
                    FactorVolskew,
                    (FactorReturnWgt.code == FactorVolskew.code) & (FactorReturnWgt.trading == FactorVolskew.trading)
                ).where(
                    FactorReturnWgt.code.in_(self.strategy_pool),
                    FactorReturnWgt.trading == self.today
                ).all()
            _f = pd.DataFrame(_f)
            _f['signal'] = factor_standardize(_f['signal'])
            _f['signal_volReturn'] = factor_standardize(_f['signal_volReturn'])
            _f['combined_signal'] = _f['signal'] + _f['signal_volReturn']  # 等权合并因子, 负向因子
            _f.sort_values(by='combined_signal', ascending=True, inplace=True)  # 正序排列

            _target = _f.iloc[:30, :].copy()
            _target['target_weight'] = 0.85 / len(_target)
            _target['limit_type'] = _target['code'].apply(lambda c: set_limit_type(c))
            _target.drop(columns=['signal', 'signal_volReturn', 'combined_signal'], inplace=True)
            _target.reset_index(drop=True, inplace=True)

            self.order.bulk_order_targetWeight(_target, result)
        return  # 返回today, frame, next_row = self.__next__() 供run()函数使用

    def after_trading(self):
        """若当日属于报告期日，则需计算当前报告期的"""
        today = self.today.strftime("%Y-%m-%d")
        if today in self.financial_date.keys():
            enddate = self.financial_date[today]  # 格式：%Y-%m-%d
            # 估值因子股票池：重资产股票=(固定资产比率+资本密集度+资产负债率+存货周转率)前50% U 剔除微利股票=净利润>1000万
            # 资本密度=总资产/营业收入; 存货周转率=营业总收入/平均存货价值 --> 平均存货价值使用当期存货值简单替代
            _sql = f"""
                with bs_quarter as (
                    select iq.ticker, iq.f_ann_date, iq.end_date, iq.fix_assets/iq.total_assets as asset_rate, iq.total_assets,
                    -- 资产负债率
                    iq.total_liab/iq.total_assets as liab_rate, iq.inventories, iq.total_assets - iq.total_liab as net_assets
                    from (
                        select *, row_number() over (
                            partition by ticker, end_date
                            order by update_flag desc, f_ann_date
                        ) as rn
                        from quant_research.financials_bs_lt
                    ) iq
                    where iq.rn=1 and iq.total_assets>0 and iq.inventories>0
                ),
                is_quarter as (
                    select iq.ticker, iq.f_ann_date, iq.end_date, iq.total_revenue, iq.n_income_attr_p
                    from (
                        select *, row_number() over (
                            partition by ticker, end_date
                            order by update_flag desc, f_ann_date
                        ) as rn
                        from quant_research."financials_incomeState_quarter"
                    ) as iq
                    where iq.rn=1 and iq.total_revenue>0
                )
                
                select a.ticker as code, a.f_ann_date, a.end_date, a.asset_rate, a.total_assets/b.total_revenue as capital_density,
                a.liab_rate, 360/(b.total_revenue/a.inventories) as turnover_days, b.n_income_attr_p, 
                b.n_income_attr_p/a.net_assets as roe_q
                from bs_quarter as a
                left join is_quarter as b
                    on a.ticker=b.ticker and a.end_date=b.end_date
                where a.end_date='{enddate}' and a.net_assets is not null
            """
            # 对重资产模式衡量指标标准化后，等权加总处理
            _raw = pd.read_sql(_sql, engine)
            _raw['capital_density'] = factor_standardize(_raw['capital_density'], extreme=False)
            _raw['liab_rate'] = factor_standardize(_raw['liab_rate'], extreme=False)
            _raw['asset_rate'] = factor_standardize(_raw['asset_rate'], extreme=False)
            _raw['turnover_days'] = factor_standardize(_raw['turnover_days'], extreme=False)
            _raw['score'] = _raw['capital_density'] + _raw['liab_rate'] + _raw['asset_rate'] + _raw['turnover_days']
            # 重资产模式取前50%的股票
            _raw.sort_values(by='score', ascending=False, inplace=True)
            _shape1 = np.floor(_raw.shape[0] * 0.5)
            pool_heavy_assets = set(_raw.iloc[:int(_shape1), 0].tolist())
            # 剔除归母净利润小于1000万的股票
            pool_profitable = set(_raw[_raw['n_income_attr_p'] > 10000000]['code'].tolist())
            pool1 = pool_heavy_assets | pool_profitable

            with get_session(engine) as session:
                _f = session.query(
                    FactorPE.trade_date,
                    FactorPE.code,
                    FactorPE.ep,
                    FactorPE.ep_pivot_250,
                    FactorPB.bp,
                    FactorPB.bp_pivot_250,
                ).join(
                    FactorPB,
                    (FactorPB.code == FactorPE.code) & (FactorPB.trade_date == FactorPE.trade_date)
                ).where(
                    FactorPE.code.in_(list(pool1)),
                    FactorPE.trade_date == self.today
                ).all()
            _f = pd.DataFrame(_f)
            _f['ep'] = factor_standardize(_f['ep'])
            _f['ep_pivot_250'] = factor_standardize(_f['ep_pivot_250'])
            _f['bp'] = factor_standardize(_f['bp'])
            _f['bp_pivot_250'] = factor_standardize(_f['bp_pivot_250'])
            _f['valuation_signal'] = _f['ep'] + _f['bp'] + _f['ep_pivot_250'] + _f['bp_pivot_250']
            _f.sort_values(by='valuation_signal', ascending=False, inplace=True)

            pool1 = set(_f.iloc[:100, :]['code'].tolist())
            # ROE-Q门槛设置,年化ROE>8
            pool_roe = set(_raw[_raw['roe_q'] > 0.02]['code'].tolist())
            pool = pool1 & pool_roe
            # 与初始股票池做交集
            self.strategy_pool = list(pool & set(self.init_pool))

            if pd.to_datetime(enddate).date() > pd.to_datetime('2016-03-31').date():
                # 对股票池使用业绩超预期因子靠前因子
                _pool = tuple(self.strategy_pool)
                _sql2 = f"""
                    select code
                    from quant_research."factor_earningSuprise"
                    where trading = '{today}' and code in {_pool}
                    order by signal desc
                    limit 100
                """
                self.strategy_pool = pd.read_sql(_sql2, engine)['code'].tolist()


# 策略运行函数
def Strategy_pbROE_momentum():
    # 获取行情数据 2024-01-01至今，中证800+中证1000成分股
    sql1 = """
        select trade_date as trading, code, close, open, vol as volume, high, low
        from quant_research.market_daily_qfq
        where quant_research.market_daily_qfq.code in (
            -- 用中证1000, 中证800成分股做为股票池
            select con_code from quant_research.index_constituent where trade_date=(
                select max(trade_date) from quant_research.index_constituent
            ) and index_code in ('000852.SH', '000906.SH')
        ) and trade_date>='2014-04-30' and trade_date < '2025-08-30'
        order by trade_date;
    """
    # 获取bench_mark信息
    sql2 = """
        select trade_date as trading, close
        from quant_research.index_daily
        where ts_code='000300.SH' and trade_date >= '2014-04-30' and trade_date < '2025-08-30'
        order by trade_date;
    """
    market_data = pd.read_sql(sql1, engine)
    bench_mark = pd.read_sql(sql2, engine)
    # 准备数据管线
    price = DataLoader(market_data=market_data)
    # 报告期数组
    financial_dates_dict = get_prev_financialdate(2014, 2025)
    # 补充2025年
    financial_dates_dict['2025-03-31'] = '2025-03-31'
    financial_dates_form = []
    # 获取调仓日期
    for i in range(2014, 2025):
        financial_dates_form.append(f'{i}-04-30')
        financial_dates_form.append(f'{i}-08-31')
        financial_dates_form.append(f'{i}-10-31')

    rebalances = get_rebalance(financial_dates_form)
    # 注入回测模型
    _strategy = PeROE(price=price, rebalance_dates=rebalances, financial_date=financial_dates_dict,
                      cash=100000000, benchmark_data=bench_mark)
    _strategy.run()
    return _strategy


if __name__ == '__main__':
    # 运行策略
    strategy_pbROE = Strategy_pbROE_momentum()
    # 获取回测结果
    print("---------------- 1.组合指标 -----------------")
    strategy_pbROE.analysis.port_analysis()
    print(strategy_pbROE.analysis.port_indicators)
    strategy_pbROE.analysis.plot_return()
